﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;

namespace MoveTheCar.Model
{
    public static class PuzzleGenerator
    {
        #region Members
        public static int NumMainCars { get; set; } // number of 'goal-cars' that needs to be placed inside the puzzle
        public static int NumSecCars { get; set; } // number of 'obstacle-cars' that needs to be placed inside the puzzle
        public static int NumMinMoves { get; set; } // the number of moves a player has to move in order to complete the puzzle
        #endregion Members

        public static ObservableCollection<Car> GeneratePuzzle(int difficulty,
            double GridSquareBaseLength,
            int GridSizeH, int GridSizeW,
            double GridX, double GridY,
            double GridSpacing, double CarBaseLength)
        {
            #region Difficulty 0 Level Generation
            if (difficulty == 0) // Tutorial-ish level, 1 car, same place, always...
            {
                ObservableCollection<Car> dif0 = new ObservableCollection<Car>();
                dif0.Add(new Car(true, State.Horizontal, CarBaseLength, GridSpacing + GridX, CarBaseLength + 2 * GridSpacing + GridY));
                return dif0;
            }
            #endregion Difficulty 0 Level Generation

            #region Main variables
            char[,] PlayGrid;
            ObservableCollection<Car> Cars;
            #endregion Main variables

            #region Difficulty Switch
            switch (difficulty)
            {
                case 1: // Easy
                    NumMainCars = 1;
                    NumSecCars = 4;
                    NumMinMoves = 2;
                    break;
                case 2: // Medium
                    NumMainCars = 1;
                    NumSecCars = 9;
                    NumMinMoves = 3;
                    break;
                case 3: // Hard
                    NumMainCars = 3;
                    NumSecCars = 17;
                    NumMinMoves = 5;
                    break;
                default: // Error
                    NumMainCars = 1;
                    NumSecCars = 1;
                    NumMinMoves = 1;
                    break;
            }
            #endregion Difficulty Switch

            #region Main loop
            do
            {
                #region Main variables initialization
                PlayGrid = new char[GridSizeW, GridSizeH]; // in x and y coordinates
                Cars = new ObservableCollection<Car>(); // the collection of cars to be added.
                #endregion Main variables initialization

                #region Initialization of 2D array
                // ' ' = empty space, '<' and '>' are horizontal obstacle cars, '^' and 'v' are vertical cars. '{' & '}' are 'goal' cars
                for (int y = 0; y < GridSizeH; y++)
                    for (int x = 0; x < GridSizeW; x++)
                        PlayGrid[x, y] = ' '; // basic initialization
                #endregion Initialization of 2D array

                #region Add goal-cars
                int placedCars = 0;
                while (placedCars != NumMainCars) // try to place the required goal-cars
                {
                    Random r = new Random();
                    int y = r.Next(0, GridSizeH);
                    while (PlayGrid[0, y] != ' ' && PlayGrid[1, y] != ' ') // check if the car can be placed
                        y = r.Next(0, GridSizeH);

                    // place it
                    PlayGrid[0, y] = '{';
                    PlayGrid[1, y] = '}';
                    placedCars++;
                }
                #endregion Add goal-cars

                #region Add obstacle-cars
                do // loop once before check, then loop while its not playable
                {
                    if (!IsWinnable(PlayGrid, GridSizeH, GridSizeW)) // if it's a not winnable game, delete the obstacle cars and reposition them.
                        for (int y = 0; y < GridSizeH; y++)
                            for (int x = 0; x < GridSizeW; x++)
                                switch (PlayGrid[x, y]) // remove only obstacle-cars, not goal-cars
                                {
                                    case '<': // horizontal obstacle car found
                                        PlayGrid[x, y] = ' ';
                                        PlayGrid[x + 1, y] = ' ';
                                        break;
                                    case '^': // vertical obstacle car found
                                        PlayGrid[x, y] = ' ';
                                        PlayGrid[x, y + 1] = ' ';
                                        break;
                                    default: // something else found
                                        break;
                                }

                    placedCars = 0;
                    Random r = new Random();
                    while (placedCars != NumSecCars) // try to place the required obstacle cars
                    {
                        int x = r.Next(0, GridSizeW);
                        int y = r.Next(0, GridSizeH);

                        while (x == GridSizeW - 1 && y == GridSizeH - 1) // make sure it's not in the corner, no matter which orientation
                        {
                            x = r.Next(0, GridSizeW);
                            y = r.Next(0, GridSizeH);
                        }

                        State orientation;
                        if (x == GridSizeW - 1) // if it's all the way to the right, it needs to be vertical
                            orientation = State.Vertical;
                        else if (y == GridSizeH - 1) // if it's all the way to the bottom, it needs to be horizontal
                            orientation = State.Horizontal;
                        else
                            if (r.NextDouble() > 0.6) // more vertical than horizontal cars
                                orientation = State.Horizontal;
                            else
                                orientation = State.Vertical;

                        if (orientation == State.Horizontal)
                        {
                            int i = 1000000;
                            while (PlayGrid[x, y] != ' ' || PlayGrid[x + 1, y] != ' ') // check if the car can be placed
                            {
                                x = r.Next(0, GridSizeW - 1); // not in the corner
                                y = r.Next(0, GridSizeH);
                                // orientation stays the same
                                if (i <= 0)
                                    break; //if it can't fit, it won't try to add
                                else
                                    i--;
                            }

                            // double check
                            if (!(PlayGrid[x, y] != ' ' || PlayGrid[x + 1, y] != ' '))
                            {
                                // place it
                                PlayGrid[x, y] = '<';
                                PlayGrid[x + 1, y] = '>';
                            }
                            placedCars++;
                        }
                        else // vertical
                        {
                            int i = 1000000;
                            while (PlayGrid[x, y] != ' ' || PlayGrid[x, y + 1] != ' ') // check if the car can be placed
                            {
                                x = r.Next(0, GridSizeW);
                                y = r.Next(0, GridSizeH - 1); // not in the corner
                                // orientation stays the same
                                if (i <= 0)
                                    break; //if it can't fit, it won't try to add
                                else
                                    i--;
                            }

                            // double check
                            if (!(PlayGrid[x, y] != ' ' || PlayGrid[x, y + 1] != ' ')) // check if the car can be placed
                            {
                                // place it
                                PlayGrid[x, y] = '^';
                                PlayGrid[x, y + 1] = 'v';
                            }
                            placedCars++;
                        }
                    }
                } while (!IsWinnable(PlayGrid, GridSizeH, GridSizeW));
                #endregion Add obstacle-cars

            } while (!PuzzleHasMinimumMoves(PlayGrid, GridSizeH, GridSizeW));
            #endregion Main loop

            #region Converting char 2D array to Cars

            for (int y = 0; y < GridSizeH; y++)
            {
                for (int x = 0; x < GridSizeW; x++)
                {
                    double PosX = GridX + (x + 1) * GridSpacing + x * CarBaseLength;
                    double PosY = GridY + (y + 1) * GridSpacing + y * CarBaseLength;

                    char s = PlayGrid[x, y];
                    switch (s)
                    {
                        case '{': // horizontal goal-car
                            Cars.Add(new Car(true, State.Horizontal, CarBaseLength, PosX, PosY));
                            break;
                        case '<': // horizontal obstacle-car
                            Cars.Add(new Car(false, State.Horizontal, CarBaseLength, PosX, PosY));
                            break;
                        case '^': // vertical obstacle-car
                            Cars.Add(new Car(false, State.Vertical, CarBaseLength, PosX, PosY));
                            break;
                        default: // do nothing, it's a space
                            break;
                    }
                }
            }
            #endregion Converting char 2D array to Cars

            return Cars;
        }

        private static bool IsWinnable(char[,] PlayGrid, int GridSizeH, int GridSizeW)
        {
            double[] numVerticalObstacleCars = new double[GridSizeW];
            for (int i = 0; i < GridSizeH; i++)
                numVerticalObstacleCars[i] = 0;
            double maxVerticalCars = GridSizeH / 2;
            double maxHorizontalCars = GridSizeW / 2;

            for (int y = 0; y < GridSizeH; y++)
            {
                double numHorizontalObstacleCars = 0.0;
                bool isGoalCarRow = false;
                for (int x = 0; x < GridSizeW; x++)
                {
                    char box = PlayGrid[x, y];
                    if (box == '{') // check if there is an goal-car in this row
                        isGoalCarRow = true;

                    if ((box == '<' || box == '>') && isGoalCarRow) // if there is a horizontal obstacle-car and it's the row of a goal car
                        return false; // the puzzle isn't winnable

                    if (box == '^' || box == 'v' || box == '<' || box == '>')
                    {
                        if (numVerticalObstacleCars[x] + 1 >= maxVerticalCars) // if there are to many cars stacked vertically
                            return false; // makes the puzzle unsolvable
                        else
                            numVerticalObstacleCars[x] += 0.5; // count how many are stacked

                        if (numHorizontalObstacleCars + 1 >= maxHorizontalCars) // when there are to many cars horizontally
                            return false; // to many cars can make the puzzle unsolvable
                        else
                            numHorizontalObstacleCars += 0.5; //count how many are on each row
                    }
                }
            }

            return true; // passed the test, puzzle is winnable.
        }

        private static bool PuzzleHasMinimumMoves(char[,] PlayGrid, int GridSizeH, int GridSizeW)
        {
            int numMoves = 0;

            for (int y = 0; y < GridSizeH; y++)
            {
                int numGoalCarsChecked = 0;
                bool isGoalCarRow = false;
                for (int x = 0; x < GridSizeW; x++)
                {
                    char box = PlayGrid[x, y];
                    if (box == '{') // check if there is an goal-car in this row
                        isGoalCarRow = true;

                    if (!(box == '{' || box == '}' || box == ' ') && isGoalCarRow) // has in the same row as the goal-car another car (vertical or horizontal)
                        if (numMoves + 1 == NumMinMoves)
                            return true;
                        else
                            numMoves++;
                }
                if (isGoalCarRow)
                    if (numGoalCarsChecked + 1 == NumMainCars)
                        break;
                    else
                        numGoalCarsChecked++;
            }

            return numMoves >= NumMinMoves;
        }
    }
}
